[#950] Announce a ReplicaOfflineMsg before it is published, not after it may have been forwarded - #978
Conversation
8175d6e to
848c47c
Compare
|
Rebased onto master now that #946 has landed. The conflict both PRs predicted is resolved and the description above is updated to match; no review had been posted yet, so nothing here answers a review comment. The two changes met in the same three files.
final CSN offlineCSN = pendingChanges.putReplicaOfflineMsg();
if (offlineCSN == null && logger.isTraceEnabled())
{
/*
* The announcement itself is made where the message is published, so nothing has to be
* reported here: a message which never reached the wire was never announced either.
*/
logger.trace("Replica " + getServerId() + " of domain baseDN=" + getBaseDN()
+ " could not announce itself offline: a change which is still in flight holds"
+ " the message back, and " + pendingChanges.size() + " change(s) are pending");
}
One case needed more than a merge, and it is worth naming.
|
|
For the record, since the run on the pre-rebase head 8175d6e went red: The map this branch moves the write of - It looks like #924; the evidence, and a second sighting of the same signature on another |
848c47c to
ec70866
Compare
|
Rebased onto master ( Only This finishes the note above about the red run on the pre-rebase head. That failure now has a name: Verified on the rebased branch rather than on the old head:
|
ec70866 to
4aeb3b3
Compare
|
Rebased onto master ( The conflict was with #976, in the final CSN offlineCSN = msg.getCSN();
replicaOfflineAnnouncer.announce(offlineCSN);
if (domain.publish(msg))
{
publishedOfflineCSN = offlineCSN;
}
else
{
// The broker wrote it to no session, so nobody will forward what was announced.
replicaOfflineAnnouncer.withdraw(offlineCSN);
}
#947 changed Tests, on the rebased head:
Both regressions were watched: with the announcement moved back behind The description above is updated to match. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The order is now the right one, and a refusal is no longer silent.
- Announcing before
domain.publish()closes the #950 window by construction: a forward reported from inside the publish can no longer run ahead of the announcement it clears. PendingChanges.ReplicaOfflineAnnouncerkeepsPendingChangesoffDSRSShutdownSync; the seam is two methods, and the tests build their own announcer through it.replicaOfflineMsgNotSentwithdraws withremove(key, value)under acsn.equalsguard, so a stale withdrawal cannot take a fresher entry with it.refuseWhilePublishingasserts from inside the publishAnswer— the race is reproduced, not waited for.
Blocking
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:115-117, :142-144 — opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java:214-224
issue (blocking): A withdrawal empties a slot that announce() has already re-used, so the sent predecessor loses its wait.
replicaOfflineMsgSent is a put(): it replaces the replica's pending entry. On one domain, within the grace period:
disableService()→broker.stop()→ CSN1 announced and published; the collocated RS still has it queued to the peer RS behind a backlog (the case the forward guard's comment at:205-210names).enableService()→broker.start()— the connect fails silently (connectionError,ReplicationBroker.java:850-853) orconnectRequiresRecoveryis raised (LDAPReplicationDomain.java:5356-5363).- Second
broker.stop()→announce(CSN2)replaces CSN1's entry →publish()returns false →withdraw(CSN2)findspending.csn.equals(CSN2)and removes the slot.
awaitReplicaOfflineMsgsForwarded() now waits for nothing and CSN1 is never forwarded before the RS goes down — #919's guarantee is gone for the RS downtime. At BASE a refused CSN2 was never announced, so CSN1's wait survived. Producers: restartService() (back-to-back, from readAssuredConfig / readFractionalConfig), the total-update disable() / enable(), followed by shutdown() or another config change.
Suggested shape — a withdrawal puts back what the announcement displaced:
// PendingOfflineMsg
/** The announcement this one displaced and which is still owed its forward; null when there was none. */
private final PendingOfflineMsg displaced;
// replicaOfflineMsgSent
replicaOfflineMsgs
.computeIfAbsent(baseDN, dn -> new ConcurrentHashMap<>())
.compute(offlineCSN.getServerId(),
(id, displaced) -> new PendingOfflineMsg(offlineCSN, System.nanoTime(), displaced));
// replicaOfflineMsgNotSent
if (pending != null && pending.csn.equals(offlineCSN))
{
if (pending.displaced != null)
{
msgs.replace(serverId, pending, pending.displaced);
}
else
{
msgs.remove(serverId, pending);
}
}And the case for the reachable order (DSRSShutdownSyncTest):
/** The shutdown's message went out; the re-enable's was refused: the first one is still owed its forward. */
@Test
public void theWithdrawalOfALaterMessageGivesTheEarlierOneItsWaitBack() throws Exception
{
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync(LONG_GRACE_PERIOD);
final CSN sentByTheShutdown = newCSN(SERVER_ID, 1);
final CSN refusedByTheBroker = newCSN(SERVER_ID, 2);
shutdownSync.replicaOfflineMsgSent(baseDN1, sentByTheShutdown);
shutdownSync.replicaOfflineMsgSent(baseDN1, refusedByTheBroker);
shutdownSync.replicaOfflineMsgNotSent(baseDN1, refusedByTheBroker);
assertThat(shutdownSync.canShutdown(baseDN1)).isFalse();
shutdownSync.replicaOfflineMsgForwarded(baseDN1, sentByTheShutdown, RS_ID);
assertThat(shutdownSync.canShutdown(baseDN1)).isTrue();
}opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/PendingChangesTest.java:90-101, :231-242
issue (blocking): No case pins that the announcement of a published message stands — "withdraw unconditionally" is green 33/33.
Measured: with withdraw(offlineCSN) run on both arms of if (domain.publish(msg)), PendingChangesTest + DSRSShutdownSyncTest pass 33/33. theReplicaOfflineMsgIsAnnouncedBeforeItIsPublished forwards from inside the publish and then asserts only canShutdown == true; the refusal cases end on true as well. The only assertFalse(canShutdown) is inside refuseWhilePublishing (:252), so "announce deleted" dies once and "withdraw always" never. That mutant undoes #919 entirely and passes CI.
/** The announcement of a message the broker took stands until a peer forwards it. */
@Test
public void theAnnouncementOfAPublishedMessageStandsUntilItIsForwarded() throws Exception
{
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
final PendingChanges pendingChanges = newPendingChanges(domainWhichPublishes(true), shutdownSync);
pendingChanges.putReplicaOfflineMsg();
assertFalse(shutdownSync.canShutdown(baseDN),
"the message went out and nobody has forwarded it yet, so the shutdown must wait for it");
}And in forwardWhilePublishing, before the forward — then case 1 pins its own name:
if (msg instanceof ReplicaOfflineMsg)
{
assertFalse(shutdownSync.canShutdown(baseDN), "the message must be announced before it is published");
shutdownSync.replicaOfflineMsgForwarded(baseDN, msg.getCSN(), RS_ID);
}Non-blocking
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:806-820
suggestion (non-blocking): The production announcer is exercised by no test — swapping replicaOfflineMsgSent and replicaOfflineMsgNotSent here survives the suite.
Every test builds its own ReplicaOfflineAnnouncer (PendingChangesTest:264-275) or announces by hand. A package-visible ShutdownSyncAnnouncer(DSRSShutdownSync, DN) in place of the anonymous class, plus one case — announce → canShutdown false, withdraw → true — pins the edge.
opendj-server-legacy/src/test/java/org/opends/server/replication/service/DSRSShutdownSyncTest.java:474-513
suggestion (non-blocking): theWaitEndsWhenTheMessageIsWithdrawn pins the wake-up only by elapsed < LONG_GRACE_PERIOD (60 s); a withdrawal that notifies without emptying the slot passes it.
assertThat(shutdownSync.canShutdown(baseDN1)).as("the withdrawn message holds nothing back").isTrue();opendj-server-legacy/src/test/java/org/opends/server/replication/service/DSRSShutdownSyncTest.java:106-122 — opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:128
todo (non-blocking): Both texts describe an interleaving that cannot happen: announce() and withdraw() run back to back under pushCommittedChanges()'s monitor (PendingChanges.java:175), one announcer per domain, one domain per baseDN per JVM — no "other thread of the domain" announces in between. The reachable second announcement is the one in the blocking issue above; theWithdrawalOfAnEarlierMessageLeavesANewerOneAlone pins the reverse order. Drop "another thread" from the javadoc and "a newer one made in the meantime" from :128, and say what the case does pin: a stale withdrawal is ignored.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:49
todo (non-blocking): "counted from the moment the message was sent" — with this PR the clock starts at the announcement, before the publish; :393 already says "announced".
PR description
suggestion (non-blocking): "what is announced is what really went out, and nothing else" is stronger than what the broker can report: Session.publish() returns silently for a pre-V8 peer (getBytes() == null, #1014) and after closeInitiated, and publish() reports true. Pre-existing (#976), only noting — "what the broker reports as written" is the claim that holds.
4aeb3b3 to
fba06c0
Compare
|
Round 1 addressed in Blocking 1 - a withdrawal emptied a slot One residue is named in the javadoc rather than handled: while CSN2 stands in CSN1's place, Blocking 2 - nothing pinned that the announcement of a published message stands. Confirmed
Production announcer untested.
"Another thread" / "a newer one made in the meantime". Both reworded. The test's javadoc now
"What really went out, and nothing else". The sentence was in the Tests, on the rebased head, class per JVM: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: Both round-1 Majors are closed, and closed the way the review hoped for.
- The displaced announcement travels with the one that displaced it:
compute()chains it inreplicaOfflineMsgSent, andreplicaOfflineMsgNotSentputs it back with an identityreplace(k, pending, displaced), so a withdrawal cannot clobber a concurrent announce either. - The mutant kills claimed in the reply hold by reading: "withdraw on both arms" dies at
PendingChangesTest:116, "announce behind the publish" dies onforwardWhilePublishing's in-AnswerassertFalse— the race is reproduced inside the mocked publish, not waited for. ShutdownSyncAnnounceras a package-private class instead of the anonymous announcer: production and the tests build the domain'sPendingChangesthrough one seam.- The trade-off is disclosed where it lives: the
replicaOfflineMsgNotSentjavadoc names the window and its bound, and the description repeats it.
issue (non-blocking): the restore case pins the CSN only; "gives the earlier one its wait back" is pinned by nothing.
opendj-server-legacy/src/test/java/org/opends/server/replication/service/DSRSShutdownSyncTest.java:157-173, opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:161
theWithdrawalOfALaterMessageGivesTheEarlierOneItsWaitBack asserts isFalse after the withdrawal and isTrue after one forward; neither looks at what came back — not the restored entry's sentTime, not its awaitedForwarders. Rebuilding the displaced entry instead of restoring it — msgs.replace(serverId, pending, new PendingOfflineMsg(pending.displaced.csn, System.nanoTime(), null)) — is green 9/9 + 26/26 (measured at head). Under it the wait restarts at the withdrawal and the first peer's forward ends a wait queued for several: both description sentences false, nothing red. The production line is right; no test would notice if it stopped being.
@Test
public void theRestoredAnnouncementIsStillOwedTheForwardsItWasQueuedFor() throws Exception
{
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync(LONG_GRACE_PERIOD);
final CSN sentByTheShutdown = newCSN(SERVER_ID, 1);
final CSN refusedByTheBroker = newCSN(SERVER_ID, 2);
shutdownSync.replicaOfflineMsgSent(baseDN1, sentByTheShutdown);
shutdownSync.replicaOfflineMsgDispatched(baseDN1, sentByTheShutdown, asList(RS_ID, OTHER_RS_ID));
shutdownSync.replicaOfflineMsgSent(baseDN1, refusedByTheBroker);
shutdownSync.replicaOfflineMsgNotSent(baseDN1, refusedByTheBroker);
shutdownSync.replicaOfflineMsgForwarded(baseDN1, sentByTheShutdown, RS_ID);
assertThat(shutdownSync.canShutdown(baseDN1))
.as("the restored message is still owed the other peer's forward")
.isFalse();
shutdownSync.replicaOfflineMsgForwarded(baseDN1, sentByTheShutdown, OTHER_RS_ID);
assertThat(shutdownSync.canShutdown(baseDN1)).isTrue();
}This alone kills the mutant. Optionally pin "what is left of its own grace period" too: GRACE_PERIOD, Sent(1), sleep most of it, Sent(2), NotSent(2), canShutdown true within the remainder rather than a full period.
issue (non-blocking): theWithdrawalOfAnEarlierMessageLeavesANewerOneAlone no longer pins the withdrawal's CSN guard.
opendj-server-legacy/src/test/java/org/opends/server/replication/service/DSRSShutdownSyncTest.java:135-148, opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:157
With && pending.csn.equals(offlineCSN) deleted, the stale NotSent(1) finds the newer entry, restores CSN 1 behind it, and canShutdown is still false — green 9/9 + 26/26 (measured). Round 1's remove killed this mutant; the restore silently un-killed it. No production road produces a stale withdrawal, so hygiene: the case says it pins the guard and is green without it. The tell is a forward of the withdrawn CSN — ignored at head, consumed under the mutant:
shutdownSync.replicaOfflineMsgNotSent(baseDN1, refusedByTheBroker);
assertThat(shutdownSync.canShutdown(baseDN1)).isFalse();
shutdownSync.replicaOfflineMsgForwarded(baseDN1, refusedByTheBroker, RS_ID);
assertThat(shutdownSync.canShutdown(baseDN1))
.as("the stale withdrawal was ignored, not turned into a restore")
.isFalse();A forward of the newer CSN does not do it: isOlderThanOrEqualTo accepts it for either entry.
suggestion (non-blocking): the "not seen" window opens at the announce, not at the publish, and that end has a one-condition fix.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:226-236, :159-162
replicaOfflineMsgForwarded is get → forwardedBy() (mutates awaitedForwarders) → identity remove(k, pending). A compute() from the next announce between the get and the remove makes the CAS fail: the entry — fully forwarded, awaited set empty — stays behind the new one as displaced. If that new message is then refused, the restore puts it back, and giveUpOn never releases an empty set: only the grace expiry ends the wait. Bounded and of the disclosed class, so not a bug — and it needs no chain walk:
// replicaOfflineMsgNotSent
if (pending.displaced != null && !pending.displaced.isFullyForwarded())
{
msgs.replace(serverId, pending, pending.displaced);
}
else
{
msgs.remove(serverId, pending);
}
// PendingOfflineMsg
/** Whether every replication server the message was queued for has forwarded it. */
private boolean isFullyForwarded()
{
final Set<Integer> awaited = awaitedForwarders;
return awaited != null && awaited.isEmpty();
}suggestion (non-blocking): the displaced chain is never pruned where the collocated RS never reports on the replica.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:122-125, :387-399
compute() chains the previous entry on every announce; the only drops are the top-entry removes (:205, :236, :265) and the restore, one step back. Where nothing dispatches or forwards on this replica — a DS-only JVM, or a replica on a remote RS, since ReplicationServerDomain dispatches only under sourceHandler.isDataServer() — every restartService(), total update or config toggle adds one ~50 B node for the life of the JVM; at the base the slot held one entry. Retention only, negligible, and an expired entry restored yields nothing anyway:
.compute(offlineCSN.getServerId(), (serverId, displaced) ->
new PendingOfflineMsg(offlineCSN, announcedAt,
displaced != null && NANOSECONDS.toMillis(announcedAt - displaced.sentTime) < gracePeriod
? displaced : null));suggestion (non-blocking): the disclosed residue has no case.
opendj-server-legacy/src/test/java/org/opends/server/replication/service/DSRSShutdownSyncTest.java
None of the 26 cases forwards while a refused announcement stands in front of a sent one; aStaleForwardDoesNotConsumeTheGracePeriodOfANewerMessage never restores. The case documents the trade-off, so a change in its shape turns red:
shutdownSync.replicaOfflineMsgSent(baseDN1, sentByTheShutdown);
shutdownSync.replicaOfflineMsgDispatched(baseDN1, sentByTheShutdown, asList(RS_ID));
shutdownSync.replicaOfflineMsgSent(baseDN1, refusedByTheBroker);
shutdownSync.replicaOfflineMsgForwarded(baseDN1, sentByTheShutdown, RS_ID); // ignored: CSN 2 stands
shutdownSync.replicaOfflineMsgNotSent(baseDN1, refusedByTheBroker);
assertThat(shutdownSync.canShutdown(baseDN1))
.as("a forward reported while the refused announcement stood is not seen")
.isFalse();nitpick (non-blocking): "the one publish the broker refuses" is the broker's retry loop.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:139-143
broker.publish(msg, retryOnFailure = true) loops — no session, connectPhaseLock, tryAcquire 500 ms — until the reconnect, the refusal or the shutdown. Your reply says "up to the reconnect"; the javadoc could say it too.
nitpick (non-blocking): a Dispatched dropped behind the refused entry gives the restored one the opposite consequence to the ones the javadoc names.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:201-207, :453-460
replicaOfflineMsgDispatched reads the top only, so a Dispatched(CSN 1, ids) arriving while the refused CSN 2 stands is dropped; the restored CSN 1 keeps awaitedForwarders == null and forwardedBy() ends the wait on the first peer's forward — an early exit, where "a forward, the loss of a peer" both lengthen it. Practically unreachable (the RS reader thread records it right after the socket read; the DS has to complete disable → enable → disable first), so a word in the javadoc, unless you want the one-node descent in Dispatched as well.
nitpick (non-blocking): the description's road for the round-1 withdrawal item names a sequence no main code runs.
PR description, "A withdrawal must not take an earlier announcement with it"
"a shutdown whose message went out, followed within the grace period by an enableService() … and then by another disableService()" — nothing calls enableService() after LDAPReplicationDomain.shutdown(). The roads are the config toggle, the total-update pair, restartService() and the locked config change; the mechanism is the same on each. If "shutdown" means the collocated RS's, say so.
…published, not after it may have been forwarded The announcement the shutdown of a collocated replication server waits on was recorded after PendingChanges.putReplicaOfflineMsg() had already put the message on the wire. A forward which won that race found nothing to clear, and the announcement which followed it was one nothing would ever remove: ReplicationServer.shutdown() then spent the whole REPLICA_OFFLINE_GRACE_PERIOD waiting for the forward of a message the topology already had. The announcement now sits where the message is published - the ReplicaOfflineMsg branch of pushCommittedChanges() - so it is in place before session.publish() is reached and the forward cannot precede it. It goes through ShutdownSyncAnnouncer, the announcer of one domain and one DSRSShutdownSync, which the domain hands its PendingChanges. Announcing at the publish site also makes it follow the publication rather than the queueing, which leaves the guard OpenIdentityPlatform#918 put around the announcement nothing to do: a message a change in flight holds back is not published, and is therefore not announced either. The trace which reports such a message stays. The broker may still refuse the message once it is announced - no usable session, a recovery pending, or stopped in between - which OpenIdentityPlatform#949 made domain.publish() report. Such an announcement is one nobody will ever forward, so it is withdrawn through the new DSRSShutdownSync.replicaOfflineMsgNotSent(), which takes back only the entry carrying that CSN and wakes the shutdown up as a forward does: what stays announced is what the broker reports as written. A withdrawal gives back what the announcement displaced. A replica announces itself offline on every disableService(), and each announcement replaces the entry of the replica, so the refused message of a disable which follows a failed re-enable within the grace period had taken the place of the message the earlier disable did send - and withdrawing it emptied the slot that message was still waiting in. PendingOfflineMsg now keeps the announcement it displaced, and the withdrawal puts it back, with its own clock and the peers it was queued for. It keeps it only while its grace period runs, or every announcement of a replica whose message nobody in this process forwards would be chained for the life of the process; and it gives back only an announcement which is still owed a forward, since the forward which released the displaced one may have been reported while the new announcement was being made, and its remove() then found the new entry in its place. New PendingChangesTest cases, all through the production announcer: the forward reported from within publish() finds the announcement in place and clears it, the announcement of a message the broker took stands until a peer forwards it, the announcement of a message the broker refused is withdrawn, and a message held back by a change in flight is never announced - neither while it waits, nor when the change which held it back completes and the message is given up on. DSRSShutdownSyncTest covers the withdrawal: it ends the wait and leaves nothing behind, it wakes a waiting shutdown up, it leaves a newer announcement of the same replica alone - and does not restore the stale one -, it gives an earlier one its wait back with the peers it was queued for and what is left of its own grace period, and a forward reported while the refused announcement stood in front is not seen.
fba06c0 to
43f3d15
Compare
|
Round 2 addressed in The restore case pinned the CSN only. Confirmed by running the mutant: with the displaced entry The stale-withdrawal case no longer pinned the guard. Confirmed: with The residue's other end. Taken: The chain is never pruned. Taken, in the shape you gave: The disclosed residue has no case. "The one publish the broker refuses". The javadoc now says where the window ends: at once on a A The description's road. Rewritten: the roads are the fractional/assured config change, Tests, on the rebased head, class per JVM:
The description above is updated to match. |
Fixes #950
The bug
LDAPReplicationDomain.publishReplicaOfflineMsg()recorded the announcement afterpendingChanges.putReplicaOfflineMsg()returned, and that call has already put the message onthe wire:
pushCommittedChanges()reachesdomain.publish(msg)->ReplicationBroker.publish()->
session.publish(msg)before it comes back.A collocated replication server which forwards the message in that window calls
DSRSShutdownSync.replicaOfflineMsgForwarded()from itsServerWriter, which finds no entry forthe replica and does nothing but notify the monitor.
replicaOfflineMsgSent()then installs aPendingOfflineMsgwhich nothing will ever remove - the forward it was waiting for has alreadyhappened.
Since #919 that record is the condition of a blocking wait:
ReplicationServer.shutdown()callsawaitReplicaOfflineMsgsForwarded()and, with a peer RS connected, spends the wholeREPLICA_OFFLINE_GRACE_PERIODon a message which is on the wire and forwarded. Nothing is lost -the topology has the announcement - it is a bounded delay of the shutdown. Before #919 the stale
record was harmless, and the ordering it depends on has been there since OPENDJ-1453.
#946 has since narrowed which messages are announced - only those which really were published -
but left the ordering alone: the announcement of a published message still follows its publish.
The window is narrow: between the return of
session.publish()and the next statement of thepublishing thread, the collocated RS has to read the socket, write the changelog, queue the
message on the peer handler and write it to the peer session. But the cost of losing the race is
precisely the delay the grace period exists to bound.
The change
The announcement moves to the point where the message is published - the
ReplicaOfflineMsgbranch of
PendingChanges.pushCommittedChanges()- through aReplicaOfflineAnnouncerthedomain hands to its
PendingChanges. It is therefore in place beforesession.publish()isreached, and the
ConcurrentHashMapit is written to gives the forwarding thread, which reads itonly after reading the socket, the visibility it needs. The forward can no longer precede it.
The announcer the domain hands over is
ShutdownSyncAnnouncer, a package-private class of onedomain and one
DSRSShutdownSync:announce()isreplicaOfflineMsgSent(),withdraw()isreplicaOfflineMsgNotSent(). It is a class rather than an anonymous one so thatPendingChangesTestbuilds its pending changes with the very announcer the domain uses, and aswap of the two calls dies there.
Announcing at the publish site, rather than before the whole
putReplicaOfflineMsg(), also meansthe announcement follows the publication instead of the queueing. A message which a change in
flight holds back (#918) is not announced at all: #946 gives up on such a message rather than
letting it out late, so there is no later publish to announce it at.
That leaves the
if (offlineCSN != null)guard #946 put around the announcement nothing to do,which is what its own description predicted: a message which is not published is not announced.
publishReplicaOfflineMsg()keeps only the trace #946 added, with the wording #976 gave it.One announcement does have to be withdrawn. Since #976
domain.publish()reports whether thebroker wrote the message, and it refuses one when it has no usable session, when a recovery is
pending, or when it is stopped in between - all after the announcement was made. Such an
announcement is one nobody will ever forward, so
pushCommittedChanges()takes it back throughthe announcer, and
DSRSShutdownSync.replicaOfflineMsgNotSent()withdraws only the entrycarrying that CSN, and wakes the shutdown up as a forward does. This is the shape #950 proposed,
and what "not fixed here: #949" of the earlier revision of this description was waiting for.
What stays announced is what the broker reports as written - not more than that:
Session.publish()returns without writing for a peer which cannot decode the message and oncethe session's close is initiated, and the broker reports both as published. That is #976's
contract, unchanged here.
A withdrawal must not take an earlier announcement with it. A replica announces itself offline
on every
disableService(), andreplicaOfflineMsgSent()replaces the entry of the replica.Every road which disables the service and enables it again - a change of the fractional or
assured configuration,
restartService(), thedisable()/enable()pair of a total update,the
restartSession()of #974 after a failed replay - can therefore, within the grace period ofa message which went out, run an
enableService()whose connect fails or raisesconnectRequiresRecovery, and then adisableService()which announces a second message thebroker refuses; withdrawing that one used to empty the slot the first one was still waiting in.
PendingOfflineMsgnow keeps the announcement it displaced, and the withdrawal puts it back: theearlier message, which did go out, keeps its wait - with its own clock, and with the peers it was
queued for.
Two bounds on what is kept. The displaced announcement is kept only while its own grace period
runs: past it, it holds nothing back any more, and keeping it would chain every announcement of
a replica whose message nobody in this process forwards - a directory server without a
collocated replication server, or connected to a remote one, where
restartSession()announcesagain every few seconds for as long as a replay keeps failing - for the life of the process. And
the withdrawal gives back only an announcement which is still owed a forward: the forward which
released the displaced one may have been reported while the new announcement was being made, so
that the forward's identity
remove()found the new entry in its place; restored, such anannouncement would hold the shutdown for the rest of its grace period, for a forward nobody will
report again. Neither bound has a case: the first changes retention and nothing observable, the
second needs the announcement to land between the two statements of
replicaOfflineMsgForwarded(), which no test can arrange without a hook.What stays not seen. Whatever is reported about the earlier message while the refused one stands
in its place is lost to it: a forward, or a peer going away, after which the shutdown waits out
what is left of the earlier message's own grace period; the recording of its peers, after which
the first forward ends its wait, as for a message no peer was recorded for. That window is the
one refused publish - at once on
connectionErrororconnectRequiresRecovery, the broker'sretry loop up to the reconnect when it has no session - and the wait it can cost is bounded by a
grace period which is already running.
aForwardReportedWhileARefusedAnnouncementStoodIsNotSeenpins the trade-off, so that a change to it is made knowingly.
A trade-off worth naming
The grace period is now counted from just before the publish instead of just after it. Normally
that is microseconds. With the send window closed the broker loops on
tryAcquire(500 ms), and aslow publish eats part of the 5 seconds before the message even leaves. The direction is the safe
one - the wait can only end earlier, never later - and
newShutdownDeadline()bounds the wholeshutdown independently.
Tests
PendingChangesTestdrives a realDSRSShutdownSyncthrough the productionShutdownSyncAnnouncer.The five cases #946 and #976 left there are kept as they were, and four are new:
theReplicaOfflineMsgIsAnnouncedBeforeItIsPublishedreports the forward from insidepublish(), which is the moment the message reaches the session, so the race is reproducedrather than waited for. It asserts from there that the announcement is already in place, and
afterwards that the forward cleared it.
theAnnouncementOfAPublishedMessageStandsUntilItIsForwardedpins that an announcement thebroker took is not withdrawn: with nobody having forwarded it, the shutdown must wait.
theAnnouncementOfAReplicaOfflineMsgTheBrokerRefusedIsWithdrawnchecks from insidepublish()that the announcement is already in place, refuses the message the way a brokerwith no session does, and asserts nothing holds the shutdown back afterwards - so it pins a
withdrawal, not an announcement which was never made.
theReplicaOfflineMsgHeldBackByAChangeInFlightIsNeverAnnouncedpins the other half: nothing isannounced while a change in flight holds the message back, and nothing is announced when that
change completes either - [#918] Record a ReplicaOfflineMsg as sent only when it really was published #946 gives up on such a message rather than letting it out late.
DSRSShutdownSyncTestgrows seven cases for the withdrawal: it ends the wait and leaves nothingbehind; it wakes a waiting shutdown up; the withdrawal of an earlier announcement leaves a newer
one of the same replica alone, and is not taken for a restore either - a forward of the
withdrawn message is ignored afterwards; the withdrawal of a later announcement gives the earlier
one its wait back, which a forward of the earlier message then ends; the restored announcement
is still owed the forwards its message was queued for, so the first of them does not end the
wait; it keeps what is left of its own grace period rather than starting a new one; and a
forward reported while the refused announcement stood is not seen.
Five mutants were run against the suite, each dying where its name says:
domain.publish()-theReplicaOfflineMsgIsAnnouncedBeforeItIsPublishedandtheAnnouncementOfAReplicaOfflineMsgTheBrokerRefusedIsWithdrawn, both on "the message must beannounced before it is published";
if (domain.publish(msg))-theAnnouncementOfAPublishedMessageStandsUntilItIsForwarded;replicaOfflineMsgSentandreplicaOfflineMsgNotSentswapped inShutdownSyncAnnouncer-three cases of
PendingChangesTest;new PendingOfflineMsg(displaced.csn, System.nanoTime(), null)inreplicaOfflineMsgNotSent-theRestoredAnnouncementIsStillOwedTheForwardsItWasQueuedForandtheRestoredAnnouncementKeepsWhatIsLeftOfItsOwnGracePeriod; with the peers copied over andonly the clock reset, the second one alone;
csn.equalsguard of the withdrawal dropped -theWithdrawalOfAnEarlierMessageLeavesANewerOneAlone, on "the stale withdrawal was ignored,not turned into a restore".
Two more survive by design, and are listed so that nobody looks for the case which kills them:
the withdrawal restoring the displaced announcement whether or not it is still owed a forward,
and the displaced announcement kept past its grace period - the bounds named above.
theWithdrawalOfALaterMessageGivesTheEarlierOneItsWaitBackwas watched failing before thedisplaced announcement was kept: "the earlier message went out and nobody has forwarded it yet -
expected false but was true".
Overlaps
for the reason it predicted; its trace, and its giving up on the message which stayed queued,
stay. Its test cases are unchanged.
enable(), which is elsewhere inLDAPReplicationDomainthanthe announcement this branch moves, so the two merged with nothing to reconcile.
with
domain.publish()reporting a refusal, an announcement made before the publish hassomething to be withdrawn for.
pushCommittedChanges()keeps reporting the CSN of the messagethe broker accepted, so
putReplicaOfflineMsg()and the trace behave as [#949] Report a ReplicaOfflineMsg the broker refused as not sent #976 left them.put()records therecipients -
replicaOfflineMsgDispatched()is a no-op without it. This change makes thathold, leaving its
awaitedForwarders == nullfallback for the replica which picked a remotereplication server; the other case that fallback named, an announcement recorded after its
message was relayed, no longer exists, and its comment says so.
second; this branch is rebased onto them (
fef4292a5f). None of them touches theannouncement, the announcer or
DSRSShutdownSync; [#952] Keep a failed state write from killing the checkpointer and hanging the shutdown #977 rewrites other parts ofLDAPReplicationDomain, and the merge was clean.